﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Reflection;
using System.Diagnostics.Contracts;

using King.Extensions;
using King.Xml;
using System.IO;

namespace King.Reflection {

    /// <summary>
    /// Applied to an assembly to supply the name of an embeded resource that contains the XML doc-comments
    /// to make visiable via extensions on MemberInfos (e.g. Type.GetDocumentation()).
    /// </summary>
    [AttributeUsage(AttributeTargets.Assembly)]
    public sealed class AssemblyDocumentationAttribute : Attribute {

        private string m_embededResource;

        /// <summary>
        /// The case sensitive name of the embeded resource containing the XML doc-comments. This name will be
        /// passed to Assembly.GetManifestResourceStream(). The name given by Visual Studio is the dot seperated name 
        /// of the assembly, folder, and file (e.g. MyAssembly.SubDir.MyAssembly.XML)
        /// </summary>
        /// <param name="embededResource">The case sensitive name of the embeded resource containing XML doc comments.</param>
        public AssemblyDocumentationAttribute(string embededResource) {
            m_embededResource = embededResource;
        }

        /// <summary>
        /// The case sensitive name of the embeded resource containing XML doc comments.
        /// </summary>
        public string EmbededResource {
            get { return m_embededResource; }
        }
    }

    public sealed class DocumentationLoadException : Exception {
        internal DocumentationLoadException(string message)
            : base(message) {
        }
    }

    internal class AssemblyDocumentation {
        internal class Documentation {
            private string m_value;
            internal Documentation(XElement xml) {
                m_value = Regex.Replace(xml.Value.Trim(), @"\s*\n\s*", Environment.NewLine);
            }
            internal string Value { get { return m_value; } }
        }

        internal static Documentation GetDocumentation(object target, bool inherit) {
            Contract.Assert(target is MemberInfo || target is ParameterInfo);

            // create assembly cache
            if (s_assemblyDocumentation == null)
                s_assemblyDocumentation = new Dictionary<Assembly, AssemblyDocumentation>();

            // get assembly
            var assembly = target is MemberInfo ?
                ((MemberInfo)target).GetAssembly() :
                ((ParameterInfo)target).GetAssembly();

            // cache assembly
            AssemblyDocumentation cached;
            if (!s_assemblyDocumentation.TryGetValue(assembly, out cached)) {
                cached = new AssemblyDocumentation(assembly);
                lock (s_assemblyDocumentation)
                    s_assemblyDocumentation[assembly] = cached;
            }

            // get reflection pointer
            var pointer = target is MemberInfo ?
                ((MemberInfo)target).GetReflectionPointer() :
                ((ParameterInfo)target).GetReflectionPointer();

            // lookup documentation with reflection pointer
            Documentation result = null;
            lock (s_assemblyDocumentation)
                result = cached.m_documentation.GetValueOrDefault(pointer);

            if (result != null || !inherit)
                return result;

            // get documentation on base member
            object baseTarget = target is MemberInfo ?
                (object)((MemberInfo)target).GetBaseMemberInfo() :
                ((ParameterInfo)target).GetBaseParameterInfo();

            if (baseTarget == null)
                return null;

            return GetDocumentation(baseTarget, inherit);
        }
        private static Dictionary<Assembly, AssemblyDocumentation> s_assemblyDocumentation;

        private Dictionary<ReflectionPointer, Documentation> m_documentation;
        private AssemblyDocumentation(Assembly assembly) {

            m_documentation = new Dictionary<ReflectionPointer, Documentation>();

            XElement xml = null;

            #region Bind To Resource
            foreach (var name in assembly.GetManifestResourceNames()) {
                if (!name.ToLower().EndsWith(".xml"))
                    continue;

                var stream = new XStreamReader(new StreamReader(assembly.GetManifestResourceStream(name)));

                if (!stream.MoveNextElement() || stream.CurrentElement.Name != "doc") continue;
                if (!stream.MoveNextElement() || stream.CurrentElement.Name != "assembly") continue;
                if (!stream.MoveNextElement() || stream.CurrentElement.Name != "name") continue;
                if (!stream.MoveNext()) continue;
                var reader = stream.Current as TextReader;
                if (reader == null || reader.ReadToEnd() != assembly.GetName().Name) continue;

                xml = XElement.Load(assembly.GetManifestResourceStream(name));
                break;
            }
            #endregion

            if (xml == null)
                return;

            var assemblies = new[] { assembly }.Concat(assembly.GetReferencedAssemblies().Select(o => Assembly.Load(o))).ToList();

            foreach (var member in xml.Descendants("member")) {

                var docName = member.Attribute("name");

                var regex = Regex.Match((string)docName,
                    @"^(?<type>\w):(?<key>(?<fullname>([a-zA-Z0-9_#.+])+)([(](?<param>[^)]*)[)])?)$");

                MemberTypes memberType = default(MemberTypes);
                switch (regex.Get("type")) {
                    case "T": memberType = MemberTypes.TypeInfo; break;
                    case "F": memberType = MemberTypes.Field; break;
                    case "E": memberType = MemberTypes.Event; break;
                    case "P": memberType = MemberTypes.Property; break;
                    case "M": memberType = MemberTypes.Method; break;
                    default: throw new NotImplementedException();
                }

                var docTypeName = regex.Get("fullname");

                var nameRegex = Regex.Match(docTypeName, 
                    "(?<namespace>([a-zA-Z0-9_.+])+)[.](?<class>([a-zA-Z0-9_+])+)" + 
                    ((memberType == MemberTypes.TypeInfo) ? null : "[.](?<name>([a-zA-Z0-9_#+])+)"));

                var ns = nameRegex.Get("namespace");
                var typeName = nameRegex.Get("class");
                var type = assembly.GetType(ns + "." + typeName);
                var prms = regex.Get("param"); 
                var parameterTypes = prms == null ? Enumerable.Empty<Type>() :
                    prms.StringSplit(",").Select(o => 
                        assemblies.Select(a => a.GetType(o)).First(x => x != null));

                MemberInfo memberInfo = null;
                if (memberType == MemberTypes.TypeInfo) {
                    memberInfo = type;
                    
                } else {
                    var memberName = nameRegex.Get("name");
                    memberName = memberName.Replace('#', '.');

                    if (memberType == MemberTypes.Event)
                        memberInfo = type.Get<EventInfo>(memberName).Single();
                    else {
                        memberInfo = type.Get<MemberInfo>(memberName, parameterTypes).Single();

                        var methodInfo = memberInfo as MethodInfo;
                        if (methodInfo != null) {
                            var returns = member.Element("returns");
                            if (returns != null)
                                m_documentation[methodInfo.ReturnParameter.GetReflectionPointer()] = new Documentation(returns);
                        }
                    }
                }

                if (member.Elements("param").Any()) {
                    foreach (var parameter in member.Elements("param")
                        .Zip(memberInfo.GetParameters(), (x, p) => new { Xml = x, Info = p }))
                        m_documentation[parameter.Info.GetReflectionPointer()] = new Documentation(parameter.Xml);
                }

                m_documentation[memberInfo.GetReflectionPointer()] = new Documentation(member.Element("summary"));
            }
        }
    }

    public class ReflectionPointer {

        private Module m_module;
        private int m_metadataToken;
        private int? m_position;
        private Type[] m_genericTypeArguments;
        private Type[] m_genericMethodArguments;

        internal ReflectionPointer(object target) {

            Type type = target as Type;

            if (target is ParameterInfo) {
                var parameterInfo = ((ParameterInfo)target);
                target = parameterInfo.Member;
                m_position = parameterInfo.Position;
            } 
            
            if (target is MemberInfo) {
                var memberInfo = (MemberInfo)target;
                m_metadataToken = memberInfo.MetadataToken;
                m_module = memberInfo.Module;

                if (type == null)
                    type = memberInfo.DeclaringType;

                if (target is MethodInfo) {
                    var methodInfo = ((MethodInfo)target);
                    if (methodInfo.IsGenericMethod)
                        m_genericMethodArguments = methodInfo.GetGenericArguments();
                }
            }

            if (type.IsGenericType)
                m_genericTypeArguments = type.GetGenericArguments();
        }

        internal object ToObject() {
            if (m_position != null) {
                var methodBase = m_module.ResolveMethod(m_metadataToken, m_genericTypeArguments, m_genericMethodArguments);
                if (m_position == -1)
                    return ((MethodInfo)methodBase).ReturnParameter;
                return methodBase.GetParameters().Skip((int)m_position);
            }

            return m_module.ResolveMember(m_metadataToken, m_genericTypeArguments, m_genericMethodArguments);
        }

        public override string ToString() {
            return ToObject().ToString();
        }
        public override bool Equals(object obj) {
            var rhs = obj as ReflectionPointer;
            if (rhs == null)
                return false;

            if (m_module != rhs.m_module)
                return false;

            if (m_metadataToken != rhs.m_metadataToken)
                return false;

            if (m_position != rhs.m_position)
                return false;

            if (!m_genericTypeArguments.AreEqual(m_genericTypeArguments))
                return false;

            if (!m_genericMethodArguments.AreEqual(rhs.m_genericMethodArguments))
                return false;

            return true;
        }
        public override int GetHashCode() {
            var result = 0;
            if (m_genericTypeArguments != null)
                result = m_genericTypeArguments.Aggregate(result, (a, t) => a ^= t.GetHashCode());
            if (m_genericMethodArguments != null)
                result = m_genericMethodArguments.Aggregate(result, (a, t) => a ^= t.GetHashCode());
            if (m_position != null)
                result ^= (int)m_position;
            return result ^ m_metadataToken.GetHashCode() ^ m_module.GetHashCode();
        }
    }
}
